/*
    A lightweight HTTP request library for Linux
    Copyright (C) 2024  Liu Tianran

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "liu_c_http_request.h"

size_t response_buffer_size = 16;

size_t calculate_request_str_len(request *request_obj) {
    size_t request_str_len = 0;
    header *headers = request_obj->headers;
    char *host, *default_host = "127.0.0.1";
    request_str_len += strlen(request_obj->method) + strlen(" ") + strlen(request_obj->path) + strlen(" ")
                       + strlen("HTTP/1.1\r\n");
    for (; lchr_is_headers_reach_end(headers); headers = &headers[1]) {
        request_str_len += strlen(headers->key) + strlen(": ") + strlen(headers->value) + strlen("\r\n");
    }
    host = lchr_headers_get(request_obj->headers, "Host");
    if (host == NULL) request_str_len += strlen("Host") + strlen(": ") + strlen(default_host) + strlen("\r\n");
    if (request_obj->body != NULL) request_str_len += strlen(request_obj->body) + strlen("\r\n");
    request_str_len += strlen("\r\n");
    return request_str_len + 1;
}

char *connect_request_str(request *request, lchr_http_req_err *err) {
    char *request_str = calloc(1, sizeof(char) * calculate_request_str_len(request)), *host,
            *default_host = "127.0.0.1";
    header *headers;
    if (request_str == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        return NULL;
    }
    strcat(request_str, request->method), strcat(request_str, " "), strcat(request_str, request->path),
            strcat(request_str, " "), strcat(request_str, "HTTP/1.1\r\n");
    for (headers = request->headers; lchr_is_headers_reach_end(headers); headers = &headers[1]) {
        strcat(request_str, headers->key), strcat(request_str, ": "), strcat(request_str, headers->value),
                strcat(request_str, "\r\n");
    }
    host = lchr_headers_get(request->headers, "Host");
    if (host == NULL)
        strcat(request_str, "Host"), strcat(request_str, ": "), strcat(request_str, default_host),
                strcat(request_str, "\r\n");
    if (request->body != NULL) strcat(request_str, request->body), strcat(request_str, "\r\n");
    strcat(request_str, "\r\n");
    *err = HTTP_REQ_ERR_NORMAL;
    return request_str;
}

char *
recv_response_str(request *request, char *request_str, unsigned int buffer_size, int *sock_fd, lchr_http_req_err *err) {
    char *host, *default_host = "127.0.0.1", *response_str = calloc(1, sizeof(char) * buffer_size);
    char *buffer = calloc(1, sizeof(char) * buffer_size);
    size_t request_str_len = calculate_request_str_len(request), response_str_len = buffer_size;
    ssize_t bytes_read;
    struct hostent *server;
    struct sockaddr_in serv_addr = {AF_INET, htons(request->port)};
    if (response_str == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        return NULL;
    }
    if (buffer == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        free(response_str);
        return NULL;
    }
    host = lchr_headers_get(request->headers, "Host");
    if (host == NULL) {
        host = default_host;
    }
    if ((*sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        *err = HTTP_REQ_ERR_SOCKET_CREATION_FAILED;
        free(response_str);
        free(buffer);
        return NULL;
    }
    server = gethostbyname(host);
    if (server == NULL) {
        *err = HTTP_REQ_ERR_NO_SUCH_HOST;
        free(response_str);
        free(buffer);
        return NULL;
    }
    strcpy((char *) &serv_addr.sin_addr.s_addr, server->h_addr_list[0]);
    if (connect(*sock_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
        *err = HTTP_REQ_ERR_CONNECTION_FAILED;
        free(response_str);
        free(buffer);
        return NULL;
    }
    if (send(*sock_fd, request_str, request_str_len, 0) < 0) {
        *err = HTTP_REQ_ERR_SEND_FAILED;
        free(response_str);
        free(buffer);
        return NULL;
    }
    while ((bytes_read = recv(*sock_fd, buffer, buffer_size - 1, 0)) > 0) {
        buffer[bytes_read] = '\0', strcat(response_str, buffer);
        response_str = realloc(response_str, response_str_len + buffer_size - 1);
        if (response_str == NULL) {
            *err = HTTP_REQ_ERR_ALLOC_FAILED;
            free(response_str);
            free(buffer);
            return NULL;
        }
        memset(&response_str[response_str_len], 0, buffer_size - 1), memset(buffer, 0, buffer_size);
        response_str_len += buffer_size - 1;
    }
    if (bytes_read < 0) {
        *err = HTTP_REQ_ERR_RECV_FAILED;
        free(response_str);
        free(buffer);
        return NULL;
    }
    free(buffer);
    *err = HTTP_REQ_ERR_NORMAL;
    return response_str;
}

response *pack_response_obj(char *response_str, lchr_http_req_err *err) {
    char *response_str_reach, *body_pos, *found_pos;
    int i, j;
    long str_pos;
    size_t header_items_amount = 0;
    response *res = calloc(1, sizeof(response));
    if (res == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        return NULL;
    }
    response_str_reach = response_str;
    str_pos = strchr(response_str_reach, ' ') - response_str_reach;
    res->http_version = calloc(1, sizeof(char) * (str_pos + 1));
    if (res->http_version == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        free(res);
        return NULL;
    }
    strncpy(res->http_version, response_str_reach, str_pos);
    response_str_reach = strchr(response_str, ' ') + 1;
    str_pos = strchr(response_str_reach, ' ') - response_str_reach;
    res->status_code = calloc(1, sizeof(char) * (str_pos + 1));
    if (res->status_code == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        free(res->http_version);
        free(res);
        return NULL;
    }
    strncpy(res->status_code, response_str_reach, str_pos);
    response_str_reach = strchr(response_str_reach, ' ') + 1;
    str_pos = strchr(response_str_reach, '\n') - response_str_reach;
    if (strchr(response_str_reach, '\r') != NULL &&
        strchr(response_str_reach, '\r') < strchr(response_str_reach, '\n')) {
        res->status_message = calloc(1, sizeof(char) * str_pos);
        if (res->status_message == NULL) {
            *err = HTTP_REQ_ERR_ALLOC_FAILED;
            free(res->status_code);
            free(res->http_version);
            free(res);
            return NULL;
        }
        strncpy(res->status_message, response_str_reach, str_pos - 1);
        response_str_reach = strchr(response_str_reach, '\n') + 1;
    } else {
        res->status_message = calloc(1, sizeof(char) * (str_pos + 1));
        if (res->status_message == NULL) {
            *err = HTTP_REQ_ERR_ALLOC_FAILED;
            free(res->status_code);
            free(res->http_version);
            free(res);
            return NULL;
        }
        strncpy(res->status_message, response_str_reach, str_pos);
        response_str_reach = strchr(response_str_reach, '\n') + 1;
    }
    if ((body_pos = strstr(response_str_reach, "\r\n\r\n")) == NULL)
        body_pos = strstr(response_str_reach, "\n\n") + 2;
    else body_pos += 4;
    if ((found_pos = strstr(response_str_reach, "\r\n")) == NULL) found_pos = strstr(response_str_reach, "\n");
    while (found_pos != NULL && found_pos < body_pos) {
        header_items_amount++;
        found_pos = strstr(found_pos, "\r\n");
        if (found_pos == NULL) {
            found_pos = strstr(found_pos, "\n");
            found_pos = found_pos == NULL ? NULL : found_pos + 4;
        } else found_pos = found_pos == NULL ? NULL : found_pos + 2;
    }
    res->headers = calloc(1, sizeof(header) * header_items_amount);
    if (res->headers == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        free(res->status_message);
        free(res->status_code);
        free(res->http_version);
        free(res);
        return NULL;
    }
    for (i = 0; i < header_items_amount - 1; i++) {
        str_pos = strstr(response_str_reach, ": ") - response_str_reach;
        res->headers[i].key = calloc(1, str_pos + 1), strncpy(res->headers[i].key, response_str_reach, str_pos);
        if (res->headers[i].key == NULL) {
            *err = HTTP_REQ_ERR_ALLOC_FAILED;
            for (j = 0; j < i; j++) free(res->headers[j].key), free(res->headers[j].value);
            free(res->headers);
            free(res->status_message);
            free(res->status_code);
            free(res->http_version);
            free(res);
            return NULL;
        }
        response_str_reach = strstr(response_str_reach, ": ") + 2;
        str_pos = strchr(response_str_reach, '\n') - response_str_reach;
        if (strchr(response_str_reach, '\r') != NULL &&
            strchr(response_str_reach, '\r') < strchr(response_str_reach, '\n')) {
            res->headers[i].value = calloc(1, str_pos);
            if (res->headers[i].value == NULL) {
                *err = HTTP_REQ_ERR_ALLOC_FAILED;
                for (j = 0; j < i; j++) free(res->headers[j].key), free(res->headers[j].value);
                free(res->headers[j].key);
                free(res->headers);
                free(res->status_message);
                free(res->status_code);
                free(res->http_version);
                free(res);
                return NULL;
            }
            strncpy(res->headers[i].value, response_str_reach, str_pos - 1);
        } else {
            res->headers[i].value = calloc(1, str_pos + 1);
            if (res->headers[i].value == NULL) {
                *err = HTTP_REQ_ERR_ALLOC_FAILED;
                for (j = 0; j < i; j++) free(res->headers[j].key), free(res->headers[j].value);
                free(res->headers[j].key);
                free(res->headers);
                free(res->status_message);
                free(res->status_code);
                free(res->http_version);
                free(res);
                return NULL;
            }
            strncpy(res->headers[i].value, response_str_reach, str_pos);
        }
        response_str_reach = strchr(response_str_reach, '\n') + 1;
    }
    res->headers[i].key = calloc(1, strlen(END_KEY) + 1), strcpy(res->headers[i].key, END_KEY);
    res->body = calloc(1, strlen(body_pos) + 1), strcpy(res->body, body_pos);
    if (res->body == NULL) {
        *err = HTTP_REQ_ERR_ALLOC_FAILED;
        for (i = 0; i <= header_items_amount; i++) free(res->headers[i].key), free(res->headers[i].value);
        free(res->headers);
        free(res->status_message);
        free(res->status_code);
        free(res->http_version);
        free(res);
        return NULL;
    }
    *err = HTTP_REQ_ERR_NORMAL;
    return res;
}

__attribute__((unused))
int lchr_is_headers_reach_end(header *headers) {
    return strcmp((const char *) headers[0].key, END_KEY) != 0;
}

__attribute__((unused))
char *lchr_headers_get(header *headers, char *key) {
    char *value_copy;
    for (; lchr_is_headers_reach_end(headers); headers = &headers[1]) {
        if (strcmp((const char *) headers[0].key, key) == 0) {
            value_copy = calloc(1, strlen(headers[0].value)), strcpy(value_copy, headers[0].value);
            return value_copy;
        }
    }
    return NULL;
}

__attribute__((unused)) header *lchr_headers(void *end_pos, ...) {
    size_t length = 0;
    va_list args;
    int i;
    char *end_key = calloc(1, strlen(END_KEY) + 1), *end_value = calloc(1, strlen("#END-VALUE#") + 1);
    char *key_reach, *value_reach;
    strcpy(end_key, END_KEY), strcpy(end_value, "#END-VALUE#");
    header *result;
    va_start(args, end_pos);
    while (va_arg(args, char *) != end_pos) length++;
    length /= 2;
    va_end(args);
    result = calloc(1, sizeof(header) * (length + 1));
    va_start(args, end_pos);
    for (i = 0; i < length; i++) {
        key_reach = va_arg(args, char *), value_reach = va_arg(args, char *);
        result[i].key = calloc(1, strlen(key_reach)), result[i].value = calloc(1, strlen(value_reach));
        strcpy(result[i].key, key_reach), strcpy(result[i].value, value_reach);
    }
    result[length].key = end_key, result[length].value = end_value;
    va_end(args);
    return result;
}

__attribute__((unused))
void lchr_set_response_buffer_size(size_t buffer_size) {
    response_buffer_size = buffer_size;
}

__attribute__((unused))
void lchr_destroy_headers(header *headers) {
    header *headers_copy = headers;
    if (headers != NULL) {
        for (; lchr_is_headers_reach_end(headers_copy); headers_copy = &headers_copy[1]) {
            free(headers_copy->key), free(headers_copy->value);
        }
        free(headers_copy->key), free(headers_copy->value);
        free(headers);
    }
}

__attribute__((unused))
void lchr_destroy_res_obj(response *res) {
    if (res != NULL) {
        free(res->http_version);
        free(res->status_code);
        free(res->status_message);
        lchr_destroy_headers(res->headers);
        free(res->body);
        free(res);
    }
}

__attribute__((unused))
void lchr_empty_res_cb(RES_CALLBACK_ARGUMENTS) {}

__attribute__((unused))
void lchr_res_cb_clean_alloc(RES_CALLBACK_ARGUMENTS) {
    CLEAN_ALLOC
}

__attribute__((unused))
void lchr_res_cb_close_socket(RES_CALLBACK_ARGUMENTS) {
    CLOSE_SOCKET
}

__attribute__((unused))
void lchr_res_cb_clean_alloc_and_close_socket(RES_CALLBACK_ARGUMENTS) {
    CLEAN_ALLOC_AND_CLOSE_SOCKET
}

__attribute__((unused)) response *
lchr_http_request(request *request_obj, response_callback res_cb) {
    int sock_fd = 0;
    lchr_http_req_err err;
    char *request_str = connect_request_str(request_obj, &err), *response_str;
    if (err == HTTP_REQ_ERR_NORMAL) {
        response *response_obj;
        response_str = recv_response_str(request_obj, request_str, response_buffer_size, &sock_fd, &err);
        if (err == HTTP_REQ_ERR_NORMAL) {
            response_obj = pack_response_obj(response_str, &err);
            if (err == HTTP_REQ_ERR_NORMAL) {
                if (res_cb != NULL) res_cb(err, request_str, response_str, request_obj, response_obj, sock_fd);
                else {
                    free(request_str);
                    free(response_str);
                }
                return response_obj;
            } else {
                if (res_cb != NULL) res_cb(err, request_str, response_str, request_obj, NULL, sock_fd);
                else {
                    free(request_str);
                    free(response_str);
                }
                return NULL;
            }
        } else {
            if (res_cb != NULL) res_cb(err, request_str, NULL, request_obj, NULL, sock_fd);
            else {
                free(request_str);
            }
            return NULL;
        }
    } else {
        if (res_cb != NULL) res_cb(err, NULL, NULL, request_obj, NULL, sock_fd);
        return NULL;
    }
}
